Skip to content

feat: multiple workspaces (recents, ⌃R switcher, multi-window)#9

Merged
resure merged 4 commits into
mainfrom
feat/multiple-workspaces
Jul 3, 2026
Merged

feat: multiple workspaces (recents, ⌃R switcher, multi-window)#9
resure merged 4 commits into
mainfrom
feat/multiple-workspaces

Conversation

@resure

@resure resure commented Jul 3, 2026

Copy link
Copy Markdown
Owner

What

Generalizes the single remembered storage choice into a workspace registry — every opened folder (plus in-browser IndexedDB on web) is remembered with recency and surfaced three ways:

  • Orb menu → "Open Recent" submenu (click = switch in place, ⌘-click = new window on desktop).
  • ⌃R switcher (VS Code-style): filter, ↵ open, ⌘↵ new window, ⌘⌫ remove; recents ordered by recency with the top row pre-highlighted, so ⌃R↵ ⌃R↵ ping-pongs between the two most recent workspaces. A pinned "Open Folder…" action row sits outside the filter.
  • One native window per workspace on desktop (several at once), with focus-if-open so the same folder never lands in two windows.

On relaunch the main window restores the last-active workspace only. Desktop is folder-first; in-browser storage is offered on the web build only.

How

  • Registry (src/storage/workspaceRegistry.ts, absorbs + deletes handlePersistence.ts): IndexedDB bumped v1→v2 with a workspaces store + a last-active pointer; a lazy, idempotent migration seeds the old single choice into a deterministic entry.
  • useNotesStorage reworked into a workspace lifecycle (bootstrap by per-window assignment → last-active → choosing; openWorkspace/openInNewWindow/removeWorkspace), with an opSeq supersession ticket on every async path (including the detached post-activation writes).
  • Rust (src-tauri/src/lib.rs): a WindowWorkspaces label→wsId map (+ a pending set so a concurrent open can't spawn a duplicate) and four commands; ws-N windows cloned from the main-window config; main-only hide-on-close; focused/visible-window-targeted menu:about. Capabilities gain ws-* + allow-destroy/allow-set-title/allow-set-theme.
  • Shared UI: useListboxNav + highlightMatch back both the switcher and the ⌘⇧M move-to picker (fixing the latter's latent Dialog-focus-manager dead-keyboard bug); useShortcuts skips global chords while a [role="dialog"] modal is open.
  • Per-workspace UI layout persists under gravity-notes:<wsId>:* localStorage keys (adopting the legacy un-namespaced value once).

Review

Includes the fixes from a max-effort /code-review pass — 15 findings verified and addressed (activation races, IndexedDB open onblocked/onversionchange, reset() clearing the launch pointer, error-surfacing on failed opens, the duplicate-window race, and more).

Test plan

  • npm test (834 passing), npm run typecheck, npm run lint (0 errors), cargo test (21 passing), npm run build — all green.
  • New suites: workspaceRegistry.test.ts, WorkspaceSwitcherDialog.test.tsx; reworked useNotesStorage + useShortcuts + dialog/TopBar/FolderGate tests.
  • Manual QA (native window can't be scripted): first-run migration + relaunch restore; Open Recent click/⌘-click; ⌃R filter/↵/⌘↵/⌘⌫; two windows independent + focus-if-open; ⌘W closes ws-N (flushes) / hides main; About only on the focused window.

🤖 Generated with Claude Code

resure and others added 4 commits July 3, 2026 12:37
Generalize the single remembered storage choice into a workspace registry:
every opened folder (plus in-browser IndexedDB on web) is remembered with
recency, surfaced as an "Open Recent" submenu in the orb menu and a VS
Code-style ⌃R switcher (filter, ↵ open, ⌘↵ new window, ⌘⌫ remove). On desktop
each workspace can open in its own native window; several at once, with
focus-if-open so the same folder never lands in two windows. Relaunch restores
the last-active workspace only.

- Registry: IndexedDB bumped v1→v2 with a `workspaces` store + last-active
  pointer (src/storage/workspaceRegistry.ts, absorbs+deletes handlePersistence.ts);
  lazy, idempotent migration of the old single choice.
- useNotesStorage reworked into a workspace lifecycle (bootstrap by per-window
  assignment → last-active → choosing; openWorkspace/openInNewWindow), with an
  opSeq supersession guard on every async path incl. the post-activation writes.
- Rust: a WindowWorkspaces label→wsId map (+ a pending set so a concurrent open
  can't spawn a duplicate) and 4 commands; ws-N windows cloned from the main
  window config; main-only hide-on-close; focused/visible-window-targeted
  menu:about.
- Desktop is folder-first; the in-browser row is offered on web only.
- Shared useListboxNav hook + highlightMatch power both the switcher and the
  move-to picker (the latter's latent dead-keyboard bug fixed along the way);
  useShortcuts skips global chords while a modal is open.

Includes fixes from a max-effort /code-review pass (15 findings: activation
races, IndexedDB open onblocked/onversionchange, reset() clearLastActive,
error-surfacing on failed opens, and more). All tests/typecheck/lint/cargo green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- open_workspace_window: rewrite the label-pruning loop as `while let`
  (clippy::while-let-loop, -D warnings)
- reformat CLAUDE.md emphasis to Prettier's `_switch_` style

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The five "one shared picker" tests open a real Gravity popup and render the
full icon catalog in jsdom (~2-3s locally). On CI's slower runner they exceed
Vitest's 5000ms default and time out. The heaviest test in the block (reopen)
already carried an explicit 15_000 timeout — extend the same to its siblings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This PR adds a batch of new DOM tests (workspace switcher, multi-window),
increasing parallel load on CI's runner and slowing every test — enough to
push the already-heavy icon-picker popup tests past Vitest's 5s default
(one flaked at 5087ms after passing at 4321ms the prior run).

Set testTimeout=15s on the dom project so every boundary-slow Gravity-render
test gets headroom, and drop the per-test 15_000 overrides I'd added to
NoteList (the global supersedes them; that file now matches main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@resure resure merged commit f746155 into main Jul 3, 2026
2 checks passed
@resure resure deleted the feat/multiple-workspaces branch July 3, 2026 12:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant